package com.example;

import org.w3c.dom.*;

import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.FileInputStream;
import java.io.StringBufferInputStream;
import java.io.StringWriter;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.security.InvalidParameterException;
import java.util.*;
import java.util.jar.JarEntry;
import java.util.jar.JarInputStream;

/**
 * User: sckomoroh
 * Date: 2/17/13
 * Time: 4:04 PM
 */

public class Converter
{
    private static Map<String, Class> classList = new HashMap<String, Class>();

    public static void addClasses(Class contractClass)
    {
        String packageName = contractClass.getPackage().getName();
        String jarFileName = contractClass.getProtectionDomain().getCodeSource().getLocation().getPath();

        packageName = packageName.replaceAll("\\.", "/");
        System.out.println("Jar " + jarFileName + " looking for " + packageName);

        try
        {
            JarInputStream jarFile = new JarInputStream(new FileInputStream(jarFileName));
            JarEntry jarEntry;

            while (true)
            {
                jarEntry = jarFile.getNextJarEntry();
                if (jarEntry == null)
                {
                    break;
                }

                if ((jarEntry.getName().startsWith(packageName)) && (jarEntry.getName().endsWith(".class")))
                {
                    String className = jarEntry.getName().replaceAll("/", "\\.").replace(".class", "");
                    addClass(className);
                    System.out.println("Found " + className);
                }
            }
        } catch (Exception e)
        {
            e.printStackTrace();
        }
    }

    public static void addClass(String className)
    {
        try
        {
            Class cls = Class.forName(className);
            ContractXmlType typeAnnotation = (ContractXmlType)cls.getAnnotation(com.contracts.ContractXmlType.class);

            if (typeAnnotation != null)
            {
                String name = typeAnnotation.name();
                classList.put(name, cls);
            }
        }
        catch (ClassNotFoundException ex)
        {
            ex.printStackTrace();
        }
    }

    public static <T> T xmlToObject(String xmlContent) throws Exception
    {
        Document doc = getDocument(xmlContent);
        Element rootElement = doc.getDocumentElement();

        String name = rootElement.getTagName();
        if (!classList.containsKey(name))
        {
            return null;
        }

        Class objectClass = classList.get(name);
        T obj = createObject(objectClass);
        buildObject(rootElement, objectClass, obj);

        return obj;
    }

    public static String objectToXml(Object object) throws Exception
    {
        Class objectClass = object.getClass();
        ContractXmlType typeAnnotation = (ContractXmlType)objectClass.getAnnotation(ContractXmlType.class);

        if (typeAnnotation == null)
        {
            return null;
        }

        String rootName = typeAnnotation.name();

        Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
        Element rootElement = doc.createElement(rootName);
        doc.appendChild(rootElement);

        convertObject(objectClass, object, rootElement);

        StringWriter sw = new StringWriter();
        TransformerFactory tf = TransformerFactory.newInstance();
        Transformer transformer = tf.newTransformer();
        transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no");
        transformer.setOutputProperty(OutputKeys.METHOD, "xml");
        transformer.setOutputProperty(OutputKeys.INDENT, "yes");
        transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");

        transformer.transform(new DOMSource(doc), new StreamResult(sw));
        return sw.toString();
    }

    private static void convertObject(Class objectClass, Object inst, Element parent) throws Exception
    {
        Document doc = parent.getOwnerDocument();

        Method[] methods = objectClass.getMethods();
        for (Method method : methods)
        {
            ContractXmlField annotation = method.getAnnotation(ContractXmlField.class);
            if (annotation == null)
            {
                continue;
            }

            if (annotation.isSetter())
            {
                continue;
            }

            Element element = doc.createElement(annotation.name());
            parent.appendChild(element);

            Class returnType = method.getReturnType();

            if (returnType.isPrimitive() || returnType == String.class)
            {
                convertPrimitive(method, annotation.name(), inst, element, doc);
            }
            else
            {
                ContractXmlType xmlType = (ContractXmlType)returnType.getAnnotation(ContractXmlType.class);

                if (xmlType != null)
                {
                    Element parentElement = doc.createElement(xmlType.name());
                    element.appendChild(parentElement);
                    Object instObj = method.invoke(inst);

                    if (xmlType.isCollection())
                    {
                        convertCollection((Collection)instObj, xmlType.name(), parentElement);
                    }
                    else
                    {
                        convertObject(returnType, instObj, parentElement);
                    }
                }
            }
        }
    }

    private static void convertCollection(Collection collectionObject, String annotationName, Element parent) throws Exception
    {
        Document doc = parent.getOwnerDocument();

        Iterator iter = collectionObject.iterator();

        while (iter.hasNext())
        {
            Object itemObject = iter.next();
            Class itemClass = itemObject.getClass();
            Element itemElement = doc.createElement(annotationName);

            convertObject(itemClass, itemObject, parent);
        }
    }

    private static void convertPrimitive(Method method, String name, Object inst, Element parent, Document doc) throws Exception
    {
        Object returnValue = method.invoke(inst);
        Text text = doc.createTextNode(returnValue.toString());

        parent.appendChild(text);
    }

    private static void buildObject(Element rootElement, Class objectClass, Object obj) throws Exception
    {
        NodeList nodeList = rootElement.getChildNodes();

        for(int i=0; i<nodeList.getLength(); i++)
        {
            Node node = nodeList.item(i);
            if (node.getNodeType() == Node.ELEMENT_NODE)
            {
                Element element = (Element)node;

                if (element.getFirstChild().getNodeType() != Node.TEXT_NODE)
                {
                    Element valueElement = (Element)element.getFirstChild();
                    String name = valueElement.getTagName();

                    if (classList.containsKey(name))
                    {
                        Class subObjectClass = classList.get(name);
                        Object subObj = createObject(subObjectClass);

                        ContractXmlType annotation = (ContractXmlType)subObjectClass.getAnnotation(ContractXmlType.class);
                        if (annotation.isCollection())
                        {
                            createCollection(valueElement, subObjectClass, (Collection)subObj);
                        }
                        else
                        {
                            buildObject(valueElement, subObjectClass, subObj);
                        }

                        String methodName = element.getTagName();
                        setValue(methodName, subObj, objectClass, obj);
                    }
                }
                else
                {
                    // create primitive
                    createPrimitive(element, objectClass, obj);
                }
            }
        }
    }

    private static <T extends Collection> void createCollection(Element element, Class<T> collectionClass, T obj) throws Exception
    {
        NodeList nodeList = element.getChildNodes();
        for (int i=0; i<nodeList.getLength(); i++)
        {
            Element itemElement = (Element)nodeList.item(i);
            String name = itemElement.getTagName();

            if (classList.containsKey(name))
            {
                Class subObjectClass = classList.get(name);
                Object subObj = createObject(subObjectClass);

                buildObject(itemElement, subObjectClass, subObj);

                obj.add(subObj);
            }
        }
    }

    private static void createPrimitive(Element element, Class objectClass, Object obj) throws Exception
    {
        String elementName = element.getTagName();
        Node textNode = element.getFirstChild();

        if (textNode.getNodeType() == Node.TEXT_NODE)
        {
            Text text = (Text)textNode;
            String value = text.getWholeText();

            setValue(elementName, value, objectClass, obj);
        }
    }

    private static Document getDocument(String xmlContent) throws Exception
    {
        StringBufferInputStream stream = new StringBufferInputStream(xmlContent);
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        return factory.newDocumentBuilder().parse(stream);
    }

    private static void setValue(String name, Object value, Class type, Object instance) throws Exception
    {
        Method[] methods =  type.getMethods();
        for (Method method : methods)
        {
            ContractXmlField xmlField = method.getAnnotation(ContractXmlField.class);

            if (xmlField != null && xmlField.isSetter())
            {
                String xmlFieldName = xmlField.name();
                if (xmlFieldName.equals(name))
                {
                    method.invoke(instance, value);

                    break;
                }
            }
        }
    }

    private static void setValue(String name, String value, Class type, Object instance) throws Exception
    {
        Method[] methods =  type.getMethods();
        for (Method method : methods)
        {
            ContractXmlField xmlField = method.getAnnotation(ContractXmlField.class);

            if (xmlField != null && xmlField.isSetter())
            {
                String xmlFieldName = xmlField.name();
                if (xmlFieldName.equals(name))
                {
                    if (method.getParameterTypes()[0] == boolean.class)
                    {
                        method.invoke(instance, Boolean.parseBoolean(value));
                    }
                    else if (method.getParameterTypes()[0] == int.class)
                    {
                        method.invoke(instance, Integer.parseInt(value));
                    }
                    else if (method.getParameterTypes()[0] == double.class)
                    {
                        method.invoke(instance, Double.parseDouble(value));
                    }
                    else
                    {
                        method.invoke(instance, value);
                    }

                    break;
                }
            }
        }
    }

    private static <T> T createObject(Class objectClass) throws Exception
    {
        T obj = null;

        Constructor[] constructors = objectClass.getConstructors();
        for(Constructor constructor : constructors)
        {
            if (constructor.getParameterTypes().length < 1)
            {
                obj = (T)constructor.newInstance(null);
                break;
            }
        }

        if (obj == null)
        {
            throw new InvalidParameterException("No default constructor was found");
        }

        return obj;
    }
}
